package main

import (
	"compress/gzip"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"
)

// 初始化静态文件处理程序。
func initStatic(mux *http.ServeMux) error {
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		handleStatic(&gzWriter{w: w, level: gzip.BestCompression}, r)
	})

	return nil
}

// 响应对静态资源的请求。
func handleStatic(w http.ResponseWriter, r *http.Request) {
	method := strings.ToUpper(r.Method)
	path := r.URL.Path

	// 除了GET以外的请求都返回 BAD_REQUEST，对于GET，调用ServeFile
	if method == "GET" {
		// TODO: 从zip文件或者tar或者tar.gz文件中读取文件。

		// 禁止访问内容文件夹的上级文件夹。
		if containsDotDot(path) {
			http.Error(w, "Invalid URL path", http.StatusBadRequest)
			return
		}

		// 禁止访问配置文件。
		if strings.EqualFold(path, "/"+DefaultConfigFile) {
			http.Error(w, "Forbidden", http.StatusForbidden)
			return
		}

		// 将较长的URL重写为较短的URL。
		if newLocation, shouldRedirect := rewriteShortURL(w, path, r.URL.RawQuery); shouldRedirect {
			http.Redirect(w, r, newLocation, http.StatusPermanentRedirect)
			return
		}

		// 如果以斜线结尾，那么自动加上默认文档名。
		if strings.HasSuffix(path, "/") {
			path = path + defaultDocuments[0]
		}

		// 如果未指定文档扩展名，那么自动加上默认扩展名。
		ext := filepath.Ext(path)
		if ext == "" {
			ext = defaultDocumentExts[0]
			path = path + ext
		}

		// 尝试通过请求的头部来确定静态资源的本地目录。
		locale := parseLocale(r.Header.Get("Accept-Language"))
		var localePath string
		if locale != "" {
			localePath = filepath.Clean(filepath.Join(configuration.Base, strings.ToLower(locale)))
			if _, err := os.Stat(localePath); os.IsNotExist(err) {
				// 如果找不到指定的本地目录则使用基础路径。
				localePath = configuration.Base
			}
		} else {
			// 如果未指定本地目录则使用基础路径。
			localePath = configuration.Base
		}

		// 通过配置中的根目录计算出完整的文件路径。
		fullPath := filepath.Clean(filepath.Join(localePath, path[1:]))

		httpLog("GET", path, fullPath)

		// 尝试获取本地文件的状态信息。
		ld, err := os.Stat(fullPath)
		if err != nil {
			msg, code := toHTTPError(err)
			http.Error(w, msg, code)
			return
		}

		// 检查 If-Modified-Since 头。
		ims := r.Header.Get("If-Modified-Since")
		if ims != "" {
			if imst, err := http.ParseTime(ims); err == nil {
				if ld.ModTime().Before(imst.Add(time.Duration(1) * time.Second)) {
					w.WriteHeader(http.StatusNotModified)
					return
				}
			} // 返回400错误，表示不可识别的头部。
		}

		// 尝试打开本地文件并读取内容。
		lf, err := os.Open(fullPath)
		if err != nil {
			msg, code := toHTTPError(err)
			http.Error(w, msg, code)
			return
		}
		defer lf.Close()

		// 默认缓存30天
		w.Header().Set("Cache-Control", "public, max-age=2592000")

		contentSize := ld.Size()
		if mimeType, ok := allMimeTypes[strings.ToLower(ext)]; ok {
			if strings.HasPrefix(mimeType, "text/html") {
				// 对于HTML页面文件，将缓存时间缩短到60秒。
				// 因为入口文件的文件名不能随意修改（或加入hash），所以不能缓存太长时间。
				w.Header().Set("Cache-Control", "public, max-age=60")
			}
			w.Header().Set("Content-Type", mimeType)
		}

		w.Header().Set("Content-Length", fmt.Sprintf("%d", contentSize))
		fModTime := ld.ModTime()
		if !fModTime.IsZero() {
			w.Header().Set("Last-Modified", fModTime.UTC().Format(http.TimeFormat))
		}

		w.Header().Add("Vary", "Accept-Encoding")
		w.Header().Add("Server", AppName+" "+AppVersion)

		// 读取文件的全部内容并写入输出流。
		io.CopyN(w, lf, contentSize)

		if ww, ok := w.(http.Flusher); ok {
			ww.Flush()
		}
	} else {
		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
	}
}

// 尝试将请求URL重写为较短的URL。
// 重写规则：
// - /index.html -> /
// - /index.html -> /
// - /abc.html -> /abc
// - /abc.htm -> /abc
func rewriteShortURL(w http.ResponseWriter, path, rq string) (string, bool) {
	// 如果访问默认文档，那么发出301响应重定向到上级目录，这样可以强制浏览器记录较短的URL。
	for _, defaultDocument := range defaultDocuments {
		if strings.HasSuffix(path, "/"+defaultDocument) {
			if rq != "" {
				return "./?" + rq, true
			}

			return "./", true
		}
	}

	// 如果访问默认类型的文档，那么发出301响应重定向到无扩展名的URL，这样可以强制浏览器记录较短的URL。
	for _, defaultDocumentExt := range defaultDocumentExts {
		if strings.HasSuffix(path, defaultDocumentExt) {
			newLocation := "./" + path[:len(path)-len(defaultDocumentExt)]
			if rq != "" {
				return newLocation + "?" + rq, true
			}

			return newLocation, true
		}
	}

	return "", false
}
